AtklÄjiet progresÄ«vu video apstrÄdi pÄrlÅ«kprogrammÄ. Uzziniet, kÄ tieÅ”i piekļūt neapstrÄdÄtiem VideoFrame plakÅu datiem un ar tiem manipulÄt, izmantojot WebCodecs API, lai radÄ«tu pielÄgotus efektus un veiktu analÄ«zi.
WebCodecs VideoFrame plakÅu piekļuve: dziļa ienirÅ”ana neapstrÄdÄtu video datu manipulÄcijÄ
Gadiem ilgi augstas veiktspÄjas video apstrÄde tÄ«mekļa pÄrlÅ«kprogrammÄ Å”Ä·ita kÄ tÄls sapnis. IzstrÄdÄtÄji bieži vien bija ierobežoti ar <video> elementa un 2D Canvas API iespÄjÄm, kas, lai arÄ« jaudÄ«gas, radÄ«ja veiktspÄjas problÄmas un ierobežotu piekļuvi pamatÄ esoÅ”ajiem neapstrÄdÄtajiem video datiem. WebCodecs API parÄdīŔanÄs ir fundamentÄli mainÄ«jusi Å”o ainavu, nodroÅ”inot zema lÄ«meÅa piekļuvi pÄrlÅ«kprogrammas iebÅ«vÄtajiem mediju kodekiem. Viena no tÄs revolucionÄrÄkajÄm iezÄ«mÄm ir spÄja tieÅ”i piekļūt un manipulÄt ar atseviŔķu video kadru neapstrÄdÄtajiem datiem, izmantojot VideoFrame objektu.
Å is raksts ir visaptveroÅ”s ceļvedis izstrÄdÄtÄjiem, kuri vÄlas pÄriet tÄlÄk par vienkÄrÅ”u video atskaÅoÅ”anu. MÄs izpÄtÄ«sim VideoFrame plakÅu piekļuves sarežģītÄ«bu, skaidrosim tÄdus jÄdzienus kÄ krÄsu telpas un atmiÅas izkÄrtojums, un sniegsim praktiskus piemÄrus, lai dotu jums iespÄju veidot nÄkamÄs paaudzes pÄrlÅ«kprogrammas video lietojumprogrammas, sÄkot no reÄllaika filtriem lÄ«dz sarežģītiem datorredzes uzdevumiem.
PriekŔnosacījumi
Lai gÅ«tu maksimÄlu labumu no Ŕī ceļveža, jums ir jÄbÅ«t stabilÄm zinÄÅ”anÄm par:
- MÅ«sdienu JavaScript: Ieskaitot asinhrono programmÄÅ”anu (
async/await, Promises). - Pamata video jÄdzieni: BÅ«tu noderÄ«gi, ja bÅ«tu pazÄ«stami ar tÄdiem terminiem kÄ kadri, izŔķirtspÄja un kodeki.
- PÄrlÅ«ka API: Pieredze ar tÄdÄm API kÄ Canvas 2D vai WebGL bÅ«s noderÄ«ga, bet nav obligÄti nepiecieÅ”ama.
Video kadru, krÄsu telpu un plakÅu izpratne
Pirms mÄs iedziļinÄmies API, mums vispirms ir jÄizveido stabils mentÄlais modelis par to, kÄ video kadra dati patiesÄ«bÄ izskatÄs. DigitÄlais video ir nekustÄ«gu attÄlu jeb kadru secÄ«ba. Katrs kadrs ir pikseļu režģis, un katram pikselim ir krÄsa. KÄ Å”Ä« krÄsa tiek glabÄta, nosaka krÄsu telpa un pikseļu formÄts.
RGBA: tÄ«mekļa dzimtÄ valoda
LielÄkÄ daļa tÄ«mekļa izstrÄdÄtÄju ir pazÄ«stami ar RGBA krÄsu modeli. Katru pikseli attÄlo Äetri komponenti: sarkans, zaļŔ, zils un alfa (caurspÄ«dÄ«gums). Dati parasti tiek glabÄti atmiÅÄ savÄ«ti, kas nozÄ«mÄ, ka R, G, B un A vÄrtÄ«bas vienam pikselim tiek glabÄtas secÄ«gi:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
Å ajÄ modelÄ« viss attÄls tiek glabÄts vienÄ, nepÄrtrauktÄ atmiÅas blokÄ. MÄs to varam uzskatÄ«t par vienu datu "plakni".
YUV: video kompresijas valoda
Video kodeki tomÄr reti strÄdÄ tieÅ”i ar RGBA. Tie dod priekÅ”roku YUV (vai precÄ«zÄk, Y'CbCr) krÄsu telpÄm. Å is modelis sadala attÄla informÄciju:
- Y (Luminiscence): Spilgtuma jeb melnbaltÄ informÄcija. CilvÄka acs ir visjutÄ«gÄkÄ pret spilgtuma izmaiÅÄm.
- U (Cb) un V (Cr): Hrominances jeb krÄsu atŔķirÄ«bas informÄcija. CilvÄka acs ir mazÄk jutÄ«ga pret krÄsu detaļÄm nekÄ pret spilgtuma detaļÄm.
Å Ä« atdalīŔana ir atslÄga efektÄ«vai kompresijai. Samazinot U un V komponentu izŔķirtspÄju ā tehniku, ko sauc par hromas apakÅ”diskretizÄciju ā mÄs varam ievÄrojami samazinÄt faila izmÄru ar minimÄlu uztveramu kvalitÄtes zudumu. Tas noved pie planÄriem pikseļu formÄtiem, kur Y, U un V komponenti tiek glabÄti atseviŔķos atmiÅas blokos jeb "plaknÄs".
Bieži sastopams formÄts ir I420 (YUV 4:2:0 tips), kur katram 2x2 pikseļu blokam ir Äetri Y paraugi, bet tikai viens U un viens V paraugs. Tas nozÄ«mÄ, ka U un V plaknÄm ir puse no Y plaknes platuma un augstuma.
Å Ä«s atŔķirÄ«bas izpratne ir kritiski svarÄ«ga, jo WebCodecs sniedz jums tieÅ”u piekļuvi tieÅ”i Ŕīm plaknÄm, tieÅ”i tÄ, kÄ tÄs nodroÅ”ina dekoderis.
VideoFrame objekts: jÅ«su vÄrti uz pikseļu datiem
Å Ä«s mÄ«klas centrÄlais elements ir VideoFrame objekts. Tas attÄlo vienu video kadru un satur ne tikai pikseļu datus, bet arÄ« svarÄ«gus metadatus.
GalvenÄs VideoFrame Ä«paŔības
format: Virkne, kas norÄda pikseļu formÄtu (piem., 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Pilni kadra izmÄri, kÄ tie tiek glabÄti atmiÅÄ, ieskaitot jebkÄdu papildinÄjumu, ko pieprasa kodeks.displayWidth/displayHeight: IzmÄri, kas jÄizmanto kadra attÄloÅ”anai.timestamp: Kadra prezentÄcijas laika zÄ«mogs mikrosekundÄs.duration: Kadra ilgums mikrosekundÄs.
MaÄ£iskÄ metode: copyTo()
GalvenÄ metode neapstrÄdÄtu pikseļu datu piekļuvei ir videoFrame.copyTo(destination, options). Å Ä« asinhronÄ metode kopÄ kadra plakÅu datus jÅ«su norÄdÄ«tajÄ buferÄ«.
destination:ArrayBuffervai tipizÄts masÄ«vs (piemÄram,Uint8Array), kas ir pietiekami liels, lai saglabÄtu datus.options: Objekts, kas norÄda, kuras plaknes kopÄt un to atmiÅas izkÄrtojumu. Ja tas tiek izlaists, tas kopÄ visas plaknes vienÄ blakus esoÅ”Ä buferÄ«.
Metode atgriež solÄ«jumu (Promise), kas atrisinÄs ar PlaneLayout objektu masÄ«vu, pa vienam katrai kadra plaknei. Katrs PlaneLayout objekts satur divus bÅ«tiskus informÄcijas elementus:
offset: Baitu nobÄ«de, kur Ŕīs plaknes dati sÄkas galamÄrÄ·a buferÄ«.stride: Baitu skaits starp vienas pikseļu rindas sÄkumu un nÄkamÄs rindas sÄkumu Å”ai plaknei.
BÅ«tisks jÄdziens: solis pret platumu
Å is ir viens no visbiežÄkajiem neskaidrÄ«bu avotiem izstrÄdÄtÄjiem, kuri ir jauni zema lÄ«meÅa grafikas programmÄÅ”anÄ. JÅ«s nevarat pieÅemt, ka katra pikseļu datu rinda ir cieÅ”i viena aiz otras.
- Platums ir pikseļu skaits attÄla rindÄ.
- Solis (saukts arÄ« par soli vai rindas soli) ir baitu skaits atmiÅÄ no vienas rindas sÄkuma lÄ«dz nÄkamÄs rindas sÄkumam.
Bieži vien stride bÅ«s lielÄks par platums * baiti_uz_pikseli. Tas ir tÄpÄc, ka atmiÅa bieži tiek papildinÄta, lai saskaÅotu ar aparatÅ«ras robežÄm (piemÄram, 32 vai 64 baitu robežÄm), lai nodroÅ”inÄtu ÄtrÄku apstrÄdi ar CPU vai GPU. Jums vienmÄr jÄizmanto solis, lai aprÄÄ·inÄtu pikseļa atmiÅas adresi konkrÄtÄ rindÄ.
Soļa ignorÄÅ”ana novedÄ«s pie sagrozÄ«tiem vai deformÄtiem attÄliem un nepareizas datu piekļuves.
Praktiskais piemÄrs 1: melnbaltÄs plaknes piekļuve un attÄloÅ”ana
SÄksim ar vienkÄrÅ”u, bet spÄcÄ«gu piemÄru. LielÄkÄ daļa video tÄ«meklÄ« ir kodÄti YUV formÄtÄ, piemÄram, I420. 'Y' plakne faktiski ir pilnÄ«gs attÄla melnbaltais attÄlojums. MÄs varam izvilkt tikai Å”o plakni un attÄlot to uz audekla (canvas).
async function displayGrayscale(videoFrame) {
// PieÅemam, ka videoFrame ir YUV formÄtÄ, piemÄram, 'I420' vai 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('Å im piemÄram nepiecieÅ”ams YUV 4:2:0 planÄrais formÄts.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Y plakne vienmÄr ir pirmÄ.
// Izveidojam buferi, lai saglabÄtu tikai Y plaknes datus.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// IekopÄjam Y plakni mÅ«su buferÄ«.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Tagad yPlaneData satur neapstrÄdÄtus melnbaltos pikseļus.
// Mums tie ir jÄattÄlo. Izveidosim RGBA buferi audeklam.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// IterÄjam pÄri audekla pikseļiem un aizpildÄm tos no Y plaknes datiem.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Svarīgi: izmantojiet soli, lai atrastu pareizo avota indeksu!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// AprÄÄ·inÄm galamÄrÄ·a indeksu RGBA ImageData buferÄ«.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Sarkans
imageData.data[rgbaIndex + 1] = luma; // ZaļŔ
imageData.data[rgbaIndex + 2] = luma; // Zils
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRITISKI: VienmÄr aizveriet VideoFrame, lai atbrÄ«votu tÄ atmiÅu.
videoFrame.close();
}
Å is piemÄrs izceļ vairÄkus galvenos soļus: pareizÄ plaknes izkÄrtojuma identificÄÅ”ana, galamÄrÄ·a bufera pieŔķirÅ”ana, copyTo izmantoÅ”ana datu izvilkÅ”anai un pareiza iterÄcija pÄr datiem, izmantojot stride, lai izveidotu jaunu attÄlu.
Praktiskais piemÄrs 2: manipulÄcija uz vietas (sÄpijas filtrs)
Tagad veiksim tieÅ”u datu manipulÄciju. SÄpijas filtrs ir klasisks efekts, ko ir viegli ieviest. Å ajÄ piemÄrÄ ir vieglÄk strÄdÄt ar RGBA kadru, ko varÄtu iegÅ«t no audekla vai WebGL konteksta.
async function applySepiaFilter(videoFrame) {
// Å is piemÄrs pieÅem, ka ievades kadrs ir 'RGBA' vai 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('SÄpijas filtra piemÄram nepiecieÅ”ams RGBA kadrs.');
videoFrame.close();
return null;
}
// PieŔķiram buferi pikseļu datu glabÄÅ”anai.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA ir viena plakne
// Tagad manipulÄjam ar datiem buferÄ«.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 baiti uz pikseli (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// Alfa (frameData[pixelIndex + 3]) paliek nemainīga.
}
}
// Izveidojam *jaunu* VideoFrame ar modificÄtajiem datiem.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Neaizmirstiet aizvÄrt oriÄ£inÄlo kadru!
videoFrame.close();
return newFrame;
}
Tas demonstrÄ pilnÄ«gu lasīŔanas-modificÄÅ”anas-rakstīŔanas ciklu: izkopÄjiet datus, ciklÄ apstrÄdÄjiet tos, izmantojot soli, katram pikselim piemÄrojiet matemÄtisku transformÄciju un izveidojiet jaunu VideoFrame ar iegÅ«tajiem datiem. Å o jauno kadru pÄc tam var attÄlot uz audekla, nosÅ«tÄ«t uz VideoEncoder vai nodot citam apstrÄdes posmam.
VeiktspÄja ir svarÄ«ga: JavaScript pret WebAssembly (WASM)
IterÄcija pÄr miljoniem pikseļu katram kadram (1080p kadram ir vairÄk nekÄ 2 miljoni pikseļu jeb 8 miljoni datu punktu RGBA formÄtÄ) JavaScript valodÄ var bÅ«t lÄna. Lai gan mÅ«sdienu JS dzinÄji ir neticami Ätri, reÄllaika augstas izŔķirtspÄjas video (HD, 4K) apstrÄdei Ŕī pieeja var viegli pÄrslogot galveno pavedienu, radot raustÄ«tu lietotÄja pieredzi.
Å eit WebAssembly (WASM) kļūst par bÅ«tisku rÄ«ku. WASM ļauj palaist kodu, kas rakstÄ«ts tÄdÄs valodÄs kÄ C++, Rust vai Go, ar gandrÄ«z natÄ«vu Ätrumu pÄrlÅ«kprogrammÄ. Video apstrÄdes darbplÅ«sma kļūst Å”Äda:
- JavaScript valodÄ: Izmantojiet
videoFrame.copyTo(), lai iegÅ«tu neapstrÄdÄtus pikseļu datusArrayBuffer. - Nodot uz WASM: Nododiet atsauci uz Å”o buferi savÄ kompilÄtajÄ WASM modulÄ«. Å Ä« ir ļoti Ätra operÄcija, jo tÄ neietver datu kopÄÅ”anu.
- WASM (C++/Rust): Izpildiet savus augsti optimizÄtos attÄlu apstrÄdes algoritmus tieÅ”i uz atmiÅas bufera. Tas ir daudzkÄrt ÄtrÄk nekÄ JavaScript cikls.
- Atgriezties pie JavaScript: Kad WASM ir pabeidzis darbu, vadÄ«ba atgriežas pie JavaScript. PÄc tam varat izmantot modificÄto buferi, lai izveidotu jaunu
VideoFrame.
Jebkurai nopietnai, reÄllaika video manipulÄcijas lietojumprogrammai ā piemÄram, virtuÄlajiem foniem, objektu noteikÅ”anai vai sarežģītiem filtriem ā WebAssembly izmantoÅ”ana nav tikai opcija; tÄ ir nepiecieÅ”amÄ«ba.
DažÄdu pikseļu formÄtu apstrÄde (piem., I420, NV12)
Lai gan RGBA ir vienkÄrÅ”s, visbiežÄk jÅ«s saÅemsiet kadrus planÄros YUV formÄtos no VideoDecoder. ApskatÄ«sim, kÄ apstrÄdÄt pilnÄ«bÄ planÄru formÄtu, piemÄram, I420.
VideoFrame I420 formÄtÄ bÅ«s trÄ«s izkÄrtojuma deskriptori tÄ layout masÄ«vÄ:
layout[0]: Y plakne (luminiscence). IzmÄri ircodedWidthxcodedHeight.layout[1]: U plakne (hrominance). IzmÄri ircodedWidth/2xcodedHeight/2.layout[2]: V plakne (hrominance). IzmÄri ircodedWidth/2xcodedHeight/2.
LÅ«k, kÄ jÅ«s varÄtu iekopÄt visas trÄ«s plaknes vienÄ buferÄ«:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts ir 3 PlaneLayout objektu masīvs
console.log('Y plaknes izkÄrtojums:', layouts[0]); // { offset: 0, stride: ... }
console.log('U plaknes izkÄrtojums:', layouts[1]); // { offset: ..., stride: ... }
console.log('V plaknes izkÄrtojums:', layouts[2]); // { offset: ..., stride: ... }
// Tagad jūs varat piekļūt katrai plaknei `allPlanesData` buferī
// izmantojot tÄs konkrÄto nobÄ«di un soli.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Å
emiet vÄrÄ, ka hromas izmÄri ir uz pusi mazÄki!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('PiekļūtÄs Y plaknes izmÄrs:', yPlaneView.byteLength);
console.log('PiekļūtÄs U plaknes izmÄrs:', uPlaneView.byteLength);
videoFrame.close();
}
VÄl viens izplatÄ«ts formÄts ir NV12, kas ir daļÄji planÄrs. Tam ir divas plaknes: viena Y, un otra plakne, kur U un V vÄrtÄ«bas ir savÄ«tas (piemÄram, [U1, V1, U2, V2, ...]). WebCodecs API to apstrÄdÄ caurspÄ«dÄ«gi; VideoFrame NV12 formÄtÄ vienkÄrÅ”i bÅ«s divi izkÄrtojumi tÄ layout masÄ«vÄ.
IzaicinÄjumi un labÄkÄ prakse
Darbs Å”ajÄ zemajÄ lÄ«menÄ« ir spÄcÄ«gs, bet tas nÄk ar atbildÄ«bu.
AtmiÅas pÄrvaldÄ«ba ir vissvarÄ«gÄkÄ
VideoFrame aizÅem ievÄrojamu daudzumu atmiÅas, kas bieži tiek pÄrvaldÄ«ta Ärpus JavaScript atkritumu savÄcÄja kaudzes. Ja jÅ«s skaidri neatbrÄ«vosiet Å”o atmiÅu, jÅ«s izraisÄ«siet atmiÅas noplÅ«di, kas var avarÄt pÄrlÅ«kprogrammas cilni.
VienmÄr, vienmÄr izsauciet videoFrame.close(), kad esat pabeidzis darbu ar kadru.
AsinhronÄ daba
Visa datu piekļuve ir asinhrona. JÅ«su lietojumprogrammas arhitektÅ«rai ir pareizi jÄapstrÄdÄ solÄ«jumu (Promises) un async/await plÅ«sma, lai izvairÄ«tos no sacensÄ«bu apstÄkļiem un nodroÅ”inÄtu vienmÄrÄ«gu apstrÄdes konveijeru.
PÄrlÅ«kprogrammu saderÄ«ba
WebCodecs ir moderna API. Lai gan tÄ tiek atbalstÄ«ta visÄs lielÄkajÄs pÄrlÅ«kprogrammÄs, vienmÄr pÄrbaudiet tÄs pieejamÄ«bu un esiet informÄti par jebkÄdÄm ražotÄja specifiskÄm ievieÅ”anas detaļÄm vai ierobežojumiem. Izmantojiet funkciju noteikÅ”anu, pirms mÄÄ£inÄt izmantot API.
NoslÄgums: jauna robeža tÄ«mekļa video jomÄ
SpÄja tieÅ”i piekļūt un manipulÄt ar VideoFrame neapstrÄdÄtajiem plakÅu datiem, izmantojot WebCodecs API, ir paradigmu maiÅa tÄ«mekļa mediju lietojumprogrammÄm. Tas noÅem <video> elementa melno kasti un sniedz izstrÄdÄtÄjiem detalizÄtu kontroli, kas iepriekÅ” bija rezervÄta tikai natÄ«vajÄm lietojumprogrammÄm.
Izprotot video atmiÅas izkÄrtojuma pamatus ā plaknes, soli un krÄsu formÄtus ā un izmantojot WebAssembly jaudu veiktspÄjai kritiski svarÄ«gÄm operÄcijÄm, jÅ«s tagad varat veidot neticami sarežģītus video apstrÄdes rÄ«kus tieÅ”i pÄrlÅ«kprogrammÄ. No reÄllaika krÄsu korekcijas un pielÄgotiem vizuÄlajiem efektiem lÄ«dz klienta puses maŔīnmÄcÄ«bai un video analÄ«zei ā iespÄjas ir plaÅ”as. Augstas veiktspÄjas, zema lÄ«meÅa video laikmets tÄ«meklÄ« ir patiesi sÄcies.